﻿using AspNetCoreRateLimit;
using housingsafety.server.Helpers.RateLimitExt;
using housingsafety.server.Models;
using housingsafety.server.Svcs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Text;
using housingsafety.server.Caching.Core;
using housingsafety.server.Caching.Redis;
using housingsafety.server.Filters;
using housingsafety.server.SvcImpls;
using NetTopologySuite;
using NetTopologySuite.Features;
using Hangfire.LiteDB;
using Hangfire;
using Invio.Extensions.Authentication.JwtBearer;
using housingsafety.server.Hawk;
using System.Collections.Generic;
using AspNetCore.Proxy;

namespace housingsafety.server.Helpers
{
    public static class StartupExtensions
    {
        public static IServiceCollection AddJobWorkers(this IServiceCollection services, IConfiguration configuration, ILogger<Startup> logger)
        {
            services.AddHangfire(x =>
            {
                string basePath1 = AppContext.BaseDirectory;
                string dbFolder = Path.Combine(basePath1, "db");
                Directory.CreateDirectory(dbFolder);
                string dbHangfire = Path.Combine(dbFolder, "Hangfire.db");
                x.UseLiteDbStorage(dbHangfire, new LiteDbStorageOptions()
                {
                    QueuePollInterval = TimeSpan.FromSeconds(30)
                });
            });

            return services;
        }

        public static IServiceCollection AddCache(this IServiceCollection services, IConfiguration configuration, ILogger<Startup> logger)
        {
            IConfigurationSection config = configuration.GetSection("redis");
            services.Configure<RedisConfigManagement>(config);
            var redisConfig = config.Get<RedisConfigManagement>();

            // needed to store rate limit counters and ip rules
            services.AddMemoryCache();

            if (RedisUtils.Ping(redisConfig, logger))
            {
                services.AddSingleton<IRedisUtils, RedisUtils>();

                services.AddDistributedRedisCache(options =>
                {
                    options.Configuration = $"{redisConfig.Ip}:{redisConfig.Port},password={redisConfig.Password},connectTimeout={redisConfig.ConnectTimeout},syncTimeout={redisConfig.SyncTimeout}";
                    options.InstanceName = "FuJian";
                });

                services.AddSingleton<ICacheSerializer, JsonCacheSerializer>();
                services.AddSingleton<ICacheProvider, DistributedCacheProvider>();

                logger.LogInformation("redis server has connectted, enable Redis Cache");
            }
            else
            {
                logger.LogWarning("redis server has not running, enable MemoryCache");

                services.AddSingleton<ICacheProvider, MemoryCacheProvider>();
            }

            return services;
        }

        public static IServiceCollection AddStaticData(this IServiceCollection services, IConfiguration configuration)
        {
            IConfigurationSection config = configuration.GetSection("staticData");
            services.Configure<StaticDataConf>(config);

            return services;
        }


        public static IServiceCollection AddFileStore(this IServiceCollection services, IConfiguration configuration)
        {
            IConfigurationSection config = configuration.GetSection("fileStore");
            services.Configure<FileStoreConfig>(config);

            return services;
        }

        public static IServiceCollection AddDataBaseOper(this IServiceCollection services, IConfiguration configuration)
        {
            IConfigurationSection config = configuration.GetSection("dataBaseOper");
            services.Configure<DataBaseOperConfig>(config);

            return services;
        }

        public static IServiceCollection AddAliyunSms(this IServiceCollection services, IConfiguration configuration)
        {
            IConfigurationSection config = configuration.GetSection("aliyunSms");
            services.Configure<AliyunSmsConfig>(config);
            services.AddSingleton<ISmsHelper, AliyunSmsHelper>();

            return services;
        }

        public static IServiceCollection AddRateLimit(this IServiceCollection services, IConfiguration configuration)
        {
            //从appsettings.json获取相应配置
            services.Configure<IpRateLimitOptions>(configuration.GetSection("IpRateLimiting"));

            //注入计数器和规则存储
            //services.AddSingleton<IIpPolicyStore, DistributedCacheIpPolicyStore>();
            //services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();

            if (services.Any(x => x.ServiceType == typeof(IRedisUtils)))
            {
                //遇到的BUG，QPS达到一定的时候，访问网站非常非常慢,组件的问题。
                //ERROR System.OperationCanceledException: The operation was canceled.
                //at System.Threading.CancellationToken.ThrowOperationCanceledException()
                //at Microsoft.Extensions.Caching.Redis.RedisCache.RefreshAsync(String key, Nullable`1 absExpr, Nullable`1 sldExpr, CancellationToken token)
                //at Microsoft.Extensions.Caching.Redis.RedisCache.GetAndRefreshAsync(String key, Boolean getData, CancellationToken token)
                //at Microsoft.Extensions.Caching.Redis.RedisCache.GetAsync(String key, CancellationToken token)
                //at Microsoft.Extensions.Caching.Distributed.DistributedCacheExtensions.GetStringAsync(IDistributedCache cache, String key, CancellationToken token)
                //at AspNetCoreRateLimit.DistributedCacheRateLimitStore`1.GetAsync(String id, CancellationToken cancellationToken)
                //at AspNetCoreRateLimit.IpRateLimitProcessor.GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken)
                //at AspNetCoreRateLimit.RateLimitMiddleware`1.Invoke(HttpContext context)
                //at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
                //at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
                //at DigitalCertificateSystem.Handlers.ExceptionHandlerMiddleWare.Invoke(HttpContext context)
                services.AddSingleton<IIpPolicyStore, RedisIpPolicyStore>();
                services.AddSingleton<IRateLimitCounterStore, RedisCounterStore>();
            }
            else
            {
                // inject counter and rules stores
                services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
                services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
            }

            //配置（计数器密钥生成器）
            services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
            return services;
        }

        public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
        {
            // Swagger
            services.AddSwaggerGen(option =>
            {
                NetTopologySuite.Features.FeatureCollection fea = new NetTopologySuite.Features.FeatureCollection();
                fea.Add(new Feature()
                {
                    Geometry = NtsGeometryServices.Instance.CreateGeometryFactory().CreatePoint(
                        new NetTopologySuite.Geometries.Coordinate(0, 0)),
                    //Geometry = d.Pnt,
                    Attributes = new AttributesTable
                    {
                        // by default, an attribute with this property name
                        // will be written as the Feature's "id", instead of
                        // storing it in the "properties" of the Feature.
                        // you can change the name of the "special" property by
                        // using a different GeoJsonConverterFactory constructor
                        // (remember to update other code that uses it, though)
                        {"dotId", null},
                        {"tags",new string[0]}
                        // for anything that you want to be able to parse back,
                        // make sure to nest it at least one level.
                        //{
                        //    "dot", new AttributesTable
                        //    {
                        //        { "date", DateTime.Now },
                        //    },
                        //},
                    },
                });
                NetTopologySuite.IO.GeoJsonWriter geoJsonWriter = new NetTopologySuite.IO.GeoJsonWriter();
                var example = geoJsonWriter.Write(fea);

                option.SchemaGeneratorOptions.CustomTypeMappings.Add(typeof(NetTopologySuite.Features.FeatureCollection)
                    , () => new OpenApiSchema()
                    {
                        Example = new OpenApiRawString(example)
                    });

                var point = NtsGeometryServices.Instance.CreateGeometryFactory().CreatePoint(
                        new NetTopologySuite.Geometries.Coordinate(0, 0));
                var examplepoint = geoJsonWriter.Write(point);
                option.SchemaGeneratorOptions.CustomTypeMappings.Add(typeof(NetTopologySuite.Geometries.Point)
                    , () => new OpenApiSchema()
                    {
                        Example = new OpenApiRawString(examplepoint)
                    });

                var linePnts = new List<NetTopologySuite.Geometries.Coordinate>();
                linePnts.Add(new NetTopologySuite.Geometries.Coordinate(0, 0));
                linePnts.Add(new NetTopologySuite.Geometries.Coordinate(1, 1));
                var line = NtsGeometryServices.Instance.CreateGeometryFactory().CreateLineString(linePnts.ToArray());
                var exampleline = geoJsonWriter.Write(line);
                option.SchemaGeneratorOptions.CustomTypeMappings.Add(typeof(NetTopologySuite.Geometries.LineString)
                    , () => new OpenApiSchema()
                    {
                        Example = new OpenApiRawString(exampleline)
                    });

                var polyPnts = new List<NetTopologySuite.Geometries.Coordinate>();
                polyPnts.Add(new NetTopologySuite.Geometries.Coordinate(0, 0));
                polyPnts.Add(new NetTopologySuite.Geometries.Coordinate(1, 0));
                polyPnts.Add(new NetTopologySuite.Geometries.Coordinate(1, 1));
                polyPnts.Add(new NetTopologySuite.Geometries.Coordinate(0, 0));
                var polygon = NtsGeometryServices.Instance.CreateGeometryFactory().CreatePolygon(polyPnts.ToArray());
                var examplepolygon = geoJsonWriter.Write(polygon);
                option.SchemaGeneratorOptions.CustomTypeMappings.Add(typeof(NetTopologySuite.Geometries.Polygon)
                    , () => new OpenApiSchema()
                    {
                        Example = new OpenApiRawString(examplepolygon)
                    });

                option.DescribeAllParametersInCamelCase();                                 //开启驼峰规则
                option.DocInclusionPredicate((docName, description) => true);              //显示备注
                option.OperationFilter<AddFileParamTypesOperationFilter>();

                option.SwaggerDoc(ApiVersion.Version, new OpenApiInfo
                {
                    Version = ApiVersion.Version,
                    Title = "Zealmap API help",
                    Description = "API help for zealmap",
                    Contact = new OpenApiContact()
                    {
                        Name = "services@zealmap.com",
                        Email = "services@zealmap.com"
                    }
                });

                // include document file
                var docSwaggerFile = Path.Combine(AppContext.BaseDirectory, $"{typeof(Startup).Assembly.GetName().Name}.xml");
                if (File.Exists(docSwaggerFile))
                {
                    option.IncludeXmlComments(docSwaggerFile, true);
                }

                // Add security definitions
                option.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme
                    , new OpenApiSecurityScheme()
                    {
                        Scheme = JwtBearerDefaults.AuthenticationScheme,
                        BearerFormat = "JWT",
                        Description = "Please enter into field the word 'Bearer' followed by a space and the JWT value",
                        Name = "Authorization",
                        In = ParameterLocation.Header,
                        Type = SecuritySchemeType.ApiKey,
                    });

                option.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference()
                            {
                                Id = "Bearer",
                                Type = ReferenceType.SecurityScheme
                            }
                        }, Array.Empty<string>() }
                    });
            });
            return services;
        }

        /// <summary>
        /// 注入地名服务
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IServiceCollection AddAddressService(this IServiceCollection services, IConfiguration configuration)
        {
            IConfigurationSection config = configuration.GetSection("addressService");
            services.Configure<AddressServiceConfig>(config);
            return services;
        }
        public static IServiceCollection AddJwt(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddScoped<IUserTokenService, UserTokenService>();
            services.AddScoped<IHawkCredentialProvider, HawkCredentialProvider>();
            services.AddScoped<IAuthenticateService, TokenAuthenticationService>();

            IConfigurationSection config = configuration.GetSection("tokenManagement");
            services.Configure<TokenManagement>(config);
            var token = config.Get<TokenManagement>();

            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                if (options.SecurityTokenValidators.Count > 0)
                {
                    options.SecurityTokenValidators.Insert(0, new StoreJwtSecurityTokenHandler(options.SecurityTokenValidators[0]));
                }
                else
                {
                    options.SecurityTokenValidators.Add(new StoreJwtSecurityTokenHandler(new JwtSecurityTokenHandler()));
                }

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,//是否验证发行者
                    ValidateAudience = true,//在令牌验证期间验证受众 。
                    ValidateLifetime = true,//验证生命周期。
                    ValidateIssuerSigningKey = true,//是否调用对签名securityToken的SecurityKey进行验证。
                    ValidIssuer = token.Issuer,//将用于检查令牌的发行者是否与此发行者相同。
                    ValidAudience = token.Audience,//检查令牌的受众群体是否与此受众群体相同。
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(token.Secret))
                };
            })
            // This example shows the default options. You can set them to
            // whatever you like or you can leave out the lambda altogether.
            .AddJwtBearerQueryStringAuthentication(
                (JwtBearerQueryStringOptions options) =>
                {
                    options.QueryStringParameterName = "access_token";
                    options.QueryStringBehavior = QueryStringBehaviors.Redact;
                }
            )
            .AddHawk(
                (HawkAuthenticationOptions options) =>
                {
                    options.QueryStringParameterName = "bewit";
                    options.QueryStringBehavior = HawkQueryStringBehaviors.Redact;
                }
            );

            return services;
        }
    }
}
